﻿using System;
using System.IO;
using System.Net;
using System.Reflection;
using System.Runtime.Serialization;
using Pfz.RemoteGaming.Internal;
using Pfz.Remoting;
using Pfz.Remoting.Udp;
using Pfz.Serialization;
using Pfz.Threading;

namespace Pfz.RemoteGaming
{
	/// <summary>
	/// Class that should be used by the game server.
	/// It listens for client connections and then starts the game.
	/// </summary>
	public sealed class RemoteGameListener:
		ThreadSafeExceptionAwareDisposable
	{
		#region Static Area
			#region Constructor
				static RemoteGameListener()
				{
					_AddCommonType(typeof(RemoteGameComponent));
				}
			#endregion

			#region _AddCommonType
				private static void _AddCommonType(Type type)
				{
					foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
					{
						if (!property.CanWrite)
							continue;

						if (!property.CanRead)
							continue;

						if (!property.GetGetMethod().IsAbstract)
							continue;

						if (!property.GetSetMethod().IsAbstract)
							continue;

						_AssemblyGenerator._GetOrCreateRemotePropertyInfo(property);
					}
				}
			#endregion
		#endregion

		#region Fields
			private IConnectionListener<IConnection> _listener;
			private readonly Assembly[] _commonAssemblies;
		#endregion

		#region Constructors
			#region By Address and Port
				/// <summary>
				/// Creates a new GameListener for the given address and port.
				/// </summary>
				public RemoteGameListener(IPAddress address, int port, params Assembly[] commonAssemblies):
					this(new TcpWithUdpListener(address, port, false, 8*1024), commonAssemblies)
				{
				}
			#endregion
			#region By ConnectionListener
				/// <summary>
				/// Creates a new RemoteGameListener using the given Listener.
				/// </summary>
				public RemoteGameListener(IConnectionListener<IConnection> listener, params Assembly[] commonAssemblies)
				{
					if (listener == null)
						throw new ArgumentNullException("listener");

					if (commonAssemblies == null || commonAssemblies.Length == 0)
						throw new ArgumentException("commonAssemblies must not be null or empty. If the server is directly acessible by the client, pass the server assembly as the common one.", "commonAssemblies");

					_listener = listener;
					_commonAssemblies = commonAssemblies;
					foreach(var assembly in commonAssemblies)
					{
						if (assembly == null)
							throw new ArgumentException("commonAssemblies can't contain null values.", "commonAssemblies");

						foreach(var type in assembly.GetTypes())
						{
							if (!type.IsAbstract)
								continue;

							if (!type.IsSubclassOf(typeof(RemoteGameComponent)))
								continue;

							_AddCommonType(type);
						}
					}
				}
			#endregion
		#endregion
		#region Dispose
			/// <summary>
			/// Closes the internal listener.
			/// </summary>
			protected override void Dispose(bool disposing)
			{
				if (disposing)
				{
					Disposer.Dispose(ref _listener);
				}

				base.Dispose(disposing);
			}
		#endregion

		#region Methods
			#region _GetClientInitializationData
				internal _InitializationData _GetClientInitializationData()
				{
					var initializationData = new _InitializationData();
					initializationData._commonAssemblies = _commonAssemblies;

					RemoteGameProperty._propertiesLock.EnterReadLock();
					try
					{
						initializationData._propertyInfos = RemoteGameProperty._properties.ToArray();
					}
					finally
					{
						RemoteGameProperty._propertiesLock.ExitReadLock();
					}

					return initializationData;
				}
			#endregion
			#region _RunClient
				private void _RunClient(IConnection connection)
				{
					using(connection)
					{
						using(var participant = _OnCreateParticipant(connection))
						{
							if (participant == null)
								return;

							participant.Connection = connection;
							participant._listener = this;
							participant._Run();
						}
					}
				}
			#endregion
			#region _OnCreateParticipant
				private RemoteGameParticipant _OnCreateParticipant(IConnection connection)
				{
					RemoteGameParticipant result = null;

					var handler = ClientConnected;
					if (handler == null)
						throw new RemoteGameException("RemoteGameListener is only useful if you set its ClientConnected event.");

					var args = new RemoteGameConnectedEventArgs();
					args.Connection = connection;
					handler(this, args);
					result = args.Participant;

					if (result == null)
					{
						if (args.ShouldDisconnect)
							return null;

						throw new RemoteGameException("You must create and set a Participant to the event args or must set ShouldDisconnect to true.");
					}

					if (result.Room == null)
						throw new RemoteGameException("The Participant must be added to a Room to be useful.");

					result._commonAssemblies = _commonAssemblies;
					return result;
				}
			#endregion
			#region _OnException
				internal bool _OnException(Exception exception)
				{
					var handler = ExceptionThrown;
					if (handler == null)
						return false;

					var args = new RemoteGameExceptionEventArgs();
					args.Exception = exception;
					handler(this, args);
					return args.WasHandled;
				}
			#endregion

			#region Start
				private bool _started;
				/// <summary>
				/// Starts listening.
				/// </summary>
				public void Start()
				{
					lock(DisposeLock)
					{
						CheckUndisposed();

						var listener = _listener;
						if (_started)
							throw new RemoteGameException("Listener already started.");

						UnlimitedThreadPool.Run
						(
							() =>
							{
								try
								{
									while(true)
									{
										var client = listener.TryAccept();
										if (client == null)
										{
											Dispose();
											return;
										}

										UnlimitedThreadPool.Run(_RunClient, client);
									}
								}
								catch(Exception exception)
								{
									if (!WasDisposed)
										Dispose(exception);
								}
							}
						);

						_started = true;
					}
				}
			#endregion
		#endregion
		#region Events
			#region ClientConnected
				/// <summary>
				/// Event invoked when a new connection is accepted. Use it to configure the Tcp/IP client.
				/// </summary>
				public event EventHandler<RemoteGameConnectedEventArgs> ClientConnected;
			#endregion
			#region ExceptionThrown
				/// <summary>
				/// Event invoked when an exception is thrown by the game. Such exception can be treated, but the connection 
				/// to that client will be closed in any case.
				/// </summary>
				public event EventHandler<RemoteGameExceptionEventArgs> ExceptionThrown;
			#endregion
		#endregion
	}
}
